/*
 * Copyright (C) 2005-2008 Alfresco Software Limited.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.

 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

 * As a special exception to the terms and conditions of version 2.0 of 
 * the GPL, you may redistribute this Program in connection with Free/Libre 
 * and Open Source Software ("FLOSS") applications as described in Alfresco's 
 * FLOSS exception.  You should have received a copy of the text describing 
 * the FLOSS exception, and it is also available here: 
 * http://www.alfresco.com/legal/licensing"
 */

package org.alfresco.extension.avmversions.quartzjobs;

import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.extension.avmversions.AVMPurgeVersionsService;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.transaction.RetryingTransactionHelper.RetryingTransactionCallback;
import org.alfresco.service.transaction.TransactionService;
import org.alfresco.util.CronTriggerBean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;


/**
 * Quartz job that supports pruning of old versions of a single AVM Store.
 * 
 * In order to function, one (or more) instances of this class need to be configured in a custom Spring *-context.xml file in alfresco/extension.
 * Configuration is as follows:
 * 
 * <code>
 *  &lt;bean id="[myAvmStore].avmPurgeVersionsTrigger" class="org.alfresco.util.CronTriggerBean"&gt;
 *    &lt;property name="jobDetail"&gt;
 *      &lt;bean class="org.springframework.scheduling.quartz.JobDetailBean"&gt;
 *        &lt;property name="jobClass" value="org.alfresco.extension.avmversions.quartzjobs.AVMPurgeVersionsJob" /&gt;
 *        &lt;property name="jobDataAsMap"&gt;
 *          &lt;map&gt;
 *            &lt;entry key="transactionService"&gt;
 *              &lt;ref bean="transactionService" /&gt;
 *            &lt;/entry&gt;
 *            &lt;entry key="avmPurgeService"&gt;
 *              &lt;ref bean="extension.avmPurgeVersionsService" /&gt;
 *            &lt;/entry&gt;
 *            &lt;entry key="avmStore"               value="[myAvmStore]" /&gt;  &lt;-- AVM Store Id / DNS name of the Web Project --&gt;
 *            &lt;entry key="historyToPreserveValue" value="14" /&gt;
 *            &lt;entry key="historyToPreserveUnits" value="days" /&gt;
 *          &lt;/map&gt;
 *        &lt;/property&gt;
 *      &lt;/bean&gt;
 *    &lt;/property&gt;
 *    &lt;property name="scheduler" ref="schedulerFactory" /&gt;
 *    &lt;!-- trigger at 3:30am each day --&gt;
 *    &lt;property name="cronExpression" value="0 30 17 * * ?" /&gt;
 *  &lt;/bean&gt;
 *  </code>
 *  
 *  The configuration properties for this bean (defined in the "jobDataAsMap" property) are as follows: 
 *  <ul>
 *    <li><code>transactionService</code>: the reference to the Alfresco transaction service. Shouldn't normally be modified.</li>
 *    <li><code>avmPurgeService</code>: the reference to the underlying purge service. Shouldn't normally be modified.</li>
 *    <li><code>avmStore</code>: the id of the AVM store (the DNS name of the Web Project) that this job will operate against.
 *        You can find this out from the DNS name of the Web Project, or by looking in the store list in the Node Browser.</li>
 *    <li><code>historyToPreserveValue</code>: the number of "units" (see next parameter) to preserve in the version history.</li>
 *    <li><code>historyToPreserveUnits</code>: the units of time to apply to the "value" (see previous parameter). Valid values are: years, months, days, hours, minutes</li>
 *  </ul>
 *  
 *  The two "historyToPreserve" parameters collectively define how much of the version history will be preserved each time the job runs.  Here are some examples:
 *  <ul>
 *    <li>To preserve 7 years worth of version history, set:
 *        <ul>
 *          <li>historyToPreserveValue = 7</li>
 *          <li>historyToPreserveUnits = years</li>
 *        </ul>
 *    </li>
 *    <li>To preserve 6 months worth of version history, set:
 *        <ul>
 *          <li>historyToPreserveValue = 6</li>
 *          <li>historyToPreserveUnits = months</li>
 *        </ul>
 *    </li>
 *    <li>To preserve 2 weeks worth of version history, set:
 *        <ul>
 *          <li>historyToPreserveValue = 14</li>
 *          <li>historyToPreserveUnits = days</li>
 *        </ul>
 *    </li>
 *    <li>To preserve 12 hours worth of version history, set:
 *        <ul>
 *          <li>historyToPreserveValue = 12</li>
 *          <li>historyToPreserveUnits = hours</li>
 *        </ul>
 *    </li>
 *  </ul>
 *  
 *  Note also that the frequency of the job (ie. how often it runs) can be configured via the <code>cronExpression</code> property.  This is described in more detail in
 *  the <a href="http://www.opensymphony.com/quartz/wikidocs/CronTriggers%20Tutorial.html">Quartz documentation</a>. 
 *
 * @author Peter Monks (peter.monks@alfresco.com)
 * @version $Id$
 */
public class AVMPurgeVersionsJob
    extends CronTriggerBean
    implements Job
{
    private static Log log = LogFactory.getLog(AVMPurgeVersionsJob.class);
    
    private final static String JOB_DATA_PARAMETER_NAME_TRANSACTION_SERVICE       = "transactionService";
    private final static String JOB_DATA_PARAMETER_NAME_PURGE_SERVICE             = "avmPurgeService";
    private final static String JOB_DATA_PARAMETER_NAME_AVM_STORE                 = "avmStore";
    private final static String JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_VALUE = "historyToPreserveValue";
    private final static String JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_UNITS = "historyToPreserveUnits";
    
    private boolean                 initialised             = false;
    private TransactionService      transactionService      = null;
    private AVMPurgeVersionsService avmVersionPurgeService  = null;
    private String                  avmStore                = null;
    private int                     historyToPreserveValue  = -1;
    private int                     historyToPreserveUnits  = -1;
    

    /**
     * @see org.quartz.Job#execute(org.quartz.JobExecutionContext)
     */
    public void execute(final JobExecutionContext context)
        throws JobExecutionException
    {
        // Weird initialisation logic due to lack of dependency injection in Quartz jobs.
        synchronized(this)
        {
            if (!this.initialised)
            {
                initialiseConfiguration(context);
                this.initialised = true;
            }
        }
        
        // Run the purge logic as an admin user and within a transaction.
        AuthenticationUtil.RunAsWork<Object> purgeLogic = new AuthenticationUtil.RunAsWork<Object>()
        {
            public Object doWork()
                throws Exception
            {
                RetryingTransactionCallback<Object> expiredContentWork = new RetryingTransactionCallback<Object>()
                {
                    public Object execute()
                        throws Exception
                    {
                        int      numberOfVersionsPurged = -1;
                        Calendar calendar               = new GregorianCalendar();
                        Date     dateToPruneTo          = null;
                        
                        calendar.add(historyToPreserveUnits, -1 * historyToPreserveValue);
                        dateToPruneTo = calendar.getTime();
                        
                        numberOfVersionsPurged = avmVersionPurgeService.purgeAllBefore(avmStore, dateToPruneTo);
                        
                        log.debug("Purged " + numberOfVersionsPurged + " versions from AVM store " + avmStore + ".");
                        return null;
                     }
                };
                return transactionService.getRetryingTransactionHelper().doInTransaction(expiredContentWork);
            }
        };
         
        // Perform the work as the system user
        AuthenticationUtil.runAs(purgeLogic, AuthenticationUtil.SYSTEM_USER_NAME);
    }
    
    
    /**
     * Private method that initialises all of our state from the JobExecutionContext.
     * 
     * @param context The JobExecutionContext containing the configuration data <i>(must not be null)</i>.
     */
    private void initialiseConfiguration(final JobExecutionContext context)
    
    {
        // PRECONDITIONS
        assert context != null : "context must not be null.";
        
        // Body
        
        // Pull state from the job configuration data.
        JobDataMap jobData                    = context.getJobDetail().getJobDataMap();
        Object     transactionServiceObj      = jobData.get(JOB_DATA_PARAMETER_NAME_TRANSACTION_SERVICE);
        Object     avmVersionPurgeServiceObj  = jobData.get(JOB_DATA_PARAMETER_NAME_PURGE_SERVICE);
        Object     avmStoreObj                = jobData.get(JOB_DATA_PARAMETER_NAME_AVM_STORE);
        Object     historyToPreserveUnitsObj  = jobData.get(JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_UNITS);
        String     historyToPreserveUnitsStr  = null;
        Object     historyToPreserveValueObj  = jobData.get(JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_VALUE);

        
        // Transaction Service
        if (transactionServiceObj == null ||
            !(transactionServiceObj instanceof TransactionService))
        {
            throw new AlfrescoRuntimeException(JOB_DATA_PARAMETER_NAME_TRANSACTION_SERVICE + " must be provided and must be a reference to an instance of org.alfresco.service.transaction.TransactionService.");
        }
        
        this.transactionService = (TransactionService)transactionServiceObj;
        
        
        // AVM Version Purge Service
        if (avmVersionPurgeServiceObj == null ||
            !(avmVersionPurgeServiceObj instanceof AVMPurgeVersionsService))
        {
            throw new AlfrescoRuntimeException(JOB_DATA_PARAMETER_NAME_PURGE_SERVICE + " must be provided and must be a reference to an instance of org.alfresco.extension.avmversions.AVMVersionPurgeService.");
        }
        
        this.avmVersionPurgeService = (AVMPurgeVersionsService)avmVersionPurgeServiceObj;
        
        // AVM Store
        if (avmStoreObj == null ||
            !(avmStoreObj instanceof String))
        {
            throw new AlfrescoRuntimeException(JOB_DATA_PARAMETER_NAME_AVM_STORE + " must be provided and must be a String value.");
        }
        
        this.avmStore = (String)avmStoreObj;
        
        // History to preserve value
        if (historyToPreserveValueObj == null ||
            !(historyToPreserveValueObj instanceof String))
        {
            throw new AlfrescoRuntimeException(JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_VALUE + " must be provided and must be a positive integer value.");
        }
        
        try
        {
            this.historyToPreserveValue = Integer.valueOf((String)historyToPreserveValueObj);
        }
        catch (NumberFormatException nfe)
        {
            throw new AlfrescoRuntimeException(JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_VALUE + " is not an integer value: " + historyToPreserveValueObj, nfe);
        }
        
        if (this.historyToPreserveValue <= 0)
        {
            throw new AlfrescoRuntimeException(JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_VALUE + " must be a positive integer: " + this.historyToPreserveValue);
        }
        
        // History to preserve units
        if (historyToPreserveUnitsObj == null ||
            !(historyToPreserveUnitsObj instanceof String))
        {
            throw new AlfrescoRuntimeException(JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_UNITS + " must be provided and must be a String value.");
        }
        
        historyToPreserveUnitsStr = ((String)historyToPreserveUnitsObj).toUpperCase();
        
        if ("YEARS".equals(historyToPreserveUnitsStr))
        {
            this.historyToPreserveUnits = Calendar.YEAR;
        }
        else if ("MONTHS".equals(historyToPreserveUnitsStr))
        {
            this.historyToPreserveUnits = Calendar.MONTH;
        }
        else if ("DAYS".equals(historyToPreserveUnitsStr))
        {
            this.historyToPreserveUnits = Calendar.DAY_OF_MONTH;
        }
        else if ("HOURS".equals(historyToPreserveUnitsStr))
        {
            this.historyToPreserveUnits = Calendar.HOUR;
        }
        else if ("MINUTES".equals(historyToPreserveUnitsStr))
        {
            this.historyToPreserveUnits = Calendar.MINUTE;
        }
        else
        {
            throw new AlfrescoRuntimeException("Unrecognised value for " + JOB_DATA_PARAMETER_NAME_HISTORY_TO_PRESERVE_UNITS + ": " + historyToPreserveUnitsStr +
                                               ". Valid values are: YEARS, MONTHS, DAYS, HOURS, MINUTES.");
        }
    }
    
}
